package com.airbnb.epoxy;

import com.airbnb.epoxy.EpoxyModelGroup.Holder;
import com.harmony.recyclerview.annotation.LayoutRes;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.ComponentContainer.LayoutConfig;
import ohos.global.resource.NotExistException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

@SuppressWarnings("rawtypes")
public class EpoxyModelGroup extends EpoxyModelWithHolder<Holder> {

  protected final List<? extends EpoxyModel<?>> models;
  /** By default we save view state if any of the models need to save state. */
  private final boolean shouldSaveViewState;

  /**
   * @param layoutRes The layout to use with these models.
   * @param models    The models that will be used to bind the views in the given layout.
   */
  public EpoxyModelGroup(@LayoutRes int layoutRes, Collection<? extends EpoxyModel<?>> models) {
    this(layoutRes, new ArrayList<>(models));
  }

  /**
   * @param layoutRes The layout to use with these models.
   * @param models    The models that will be used to bind the views in the given layout.
   */
  public EpoxyModelGroup(@LayoutRes int layoutRes, EpoxyModel<?>... models) {
    this(layoutRes, new ArrayList<>(Arrays.asList(models)));
  }

  /**
   * @param layoutRes The layout to use with these models.
   * @param models    The models that will be used to bind the views in the given layout.
   */
  private EpoxyModelGroup(@LayoutRes int layoutRes, List<? extends EpoxyModel<?>> models) {
    if (models.isEmpty()) {
      throw new IllegalArgumentException("Models cannot be empty");
    }

    this.models = models;
    layout(layoutRes);
    id(models.get(0).id());

    boolean saveState = false;
    for (EpoxyModel<?> model : models) {
      if (model.shouldSaveViewState()) {
        saveState = true;
        break;
      }
    }

    shouldSaveViewState = saveState;
  }

  @Override
  public final void bind(Holder holder) {
    iterateModels(holder, new IterateModelsCallback() {
      @Override
      public void onModel(EpoxyModel model, Object boundObject, Component view) {
        setViewVisibility(model, view);
        //noinspection unchecked
        model.bind(boundObject);
      }
    });
  }

  @Override
  public final void bind(Holder holder, final List<Object> payloads) {
    iterateModels(holder, new IterateModelsCallback() {
      @Override
      public void onModel(EpoxyModel model, Object boundObject, Component view) {
        setViewVisibility(model, view);
        //noinspection unchecked
        model.bind(boundObject, payloads);
      }
    });
  }

  private static void setViewVisibility(EpoxyModel model, Component view) {
    if (model.isShown()) {
      view.setVisibility(Component.VISIBLE);
    } else {
      view.setVisibility(Component.HIDE);
    }
  }

  @Override
  public final void unbind(Holder holder) {
    iterateModels(holder, new IterateModelsCallback() {
      @Override
      public void onModel(EpoxyModel model, Object boundObject, Component view) {
        //noinspection unchecked
        model.unbind(boundObject);
      }
    });
  }

  @Override
  public void onViewAttachedToWindow(Holder holder) {
    iterateModels(holder, new IterateModelsCallback() {
      @Override
      public void onModel(EpoxyModel model, Object boundObject, Component view) {
        //noinspection unchecked
        model.onViewAttachedToWindow(boundObject);
      }
    });
  }

  @Override
  public void onViewDetachedFromWindow(Holder holder) {
    iterateModels(holder, new IterateModelsCallback() {
      @Override
      public void onModel(EpoxyModel model, Object boundObject, Component view) {
        //noinspection unchecked
        model.onViewDetachedFromWindow(boundObject);
      }
    });
  }

  private void iterateModels(Holder holder, IterateModelsCallback callback) {
    int modelCount = models.size();
    if (modelCount != holder.views.size()) {
      throw new IllegalStateException(
          "The number of models used in this group has changed. The model count must remain "
              + "constant if the same layout resource is used. If you need to change which models"
              + " are shown you can call EpoxyMode#hide() to have a model's view hidden, or use a"
              + " different layout resource for the group.");
    }

    for (int i = 0; i < modelCount; i++) {
      EpoxyModel model = models.get(i);
      Component view = holder.views.get(i);
      EpoxyHolder epoxyHolder = holder.holders.get(i);
      Object objectToBind = (model instanceof EpoxyModelWithHolder) ? epoxyHolder : view;

      callback.onModel(model, objectToBind, view);
    }
  }

  private interface IterateModelsCallback {
    void onModel(EpoxyModel model, Object boundObject, Component view);
  }

  @Override
  public int getSpanSize(int totalSpanCount, int position, int itemCount) {
    // Defaults to using the span size of the first model. Override this if you need to customize it
    return models.get(0).getSpanSizeInternal(totalSpanCount, position, itemCount);
  }

  @Override
  protected final int getDefaultLayout() {
    throw new UnsupportedOperationException(
        "You should set a layout with layout(...) instead of using this.");
  }

  @Override
  public boolean shouldSaveViewState() {
    // By default state is saved if any of the models have saved state enabled.
    // Override this if you need custom behavior.
    return shouldSaveViewState;
  }

  /**
   * Whether the layout params set on the view stub for the given model should be carried over to
   * the model's view. Default is true
   * <p>
   * Set this to false if you want the layout params on the model's layout resource to be kept.
   *
   * @param model         The model who's view is being created
   * @param modelPosition The position of the model in the models list
   */
  protected boolean useViewStubLayoutParams(EpoxyModel<?> model, int modelPosition) {
    return true;
  }

  @Override
  protected final Holder createNewHolder() {
    return new Holder();
  }

  protected class Holder extends EpoxyHolder {
    private List<Component> views;
    private List<EpoxyHolder> holders;

    @Override
    protected void bindView(Component itemView) {
      if (!(itemView instanceof ComponentContainer)) {
        throw new IllegalStateException(
            "The layout provided to EpoxyModelGroup must be a ViewGroup");
      }
      ComponentContainer groupView = (ComponentContainer) itemView;

      int modelCount = models.size();
      views = new ArrayList<>(modelCount);
      holders = new ArrayList<>(modelCount);

      boolean useViewStubs = groupView.getChildCount() != 0;
      for (int i = 0; i < models.size(); i++) {
        EpoxyModel model = models.get(i);
        Component view;
        if (useViewStubs) {
          view = replaceNextViewStub(groupView, model, useViewStubLayoutParams(model, i));
        } else {
          view = createAndAddView(groupView, model);
        }

        if (model instanceof EpoxyModelWithHolder) {
          EpoxyHolder holder = ((EpoxyModelWithHolder) model).createNewHolder();
          holder.bindView(view);
          holders.add(holder);
        } else {
          holders.add(null);
        }

        views.add(view);
      }
    }

    private Component createAndAddView(ComponentContainer groupView, EpoxyModel<?> model) {
      Component modelView = null;
      try {
        modelView = model.buildView(groupView);
      } catch (IOException e) {
        e.printStackTrace();
      }
      LayoutConfig modelLayoutParams = modelView.getLayoutConfig();
      if (modelLayoutParams != null) {
        groupView.addComponent(modelView, modelLayoutParams);
      } else {
        groupView.addComponent(modelView);
      }
      return modelView;
    }

    private Component replaceNextViewStub(ComponentContainer groupView, EpoxyModel<?> model,
        boolean useStubLayoutParams) {
      ViewStubData stubData = getNextViewStubPosition(groupView);
      if (stubData == null) {
        throw new IllegalStateException(
            "Your layout should provide a ViewStub for each model to be inflated into.");
      }

      stubData.viewGroup.removeComponent(stubData.viewStub);
      Component modelView = null;
      try {
        modelView = model.buildView(stubData.viewGroup);
      } catch (IOException e) {
        e.printStackTrace();
      }

      // Carry over the stub id manually since we aren't inflating via the stub
      int inflatedId = stubData.viewStub.getId();
      if (inflatedId != Component.ID_DEFAULT) {
        modelView.setId(inflatedId);
      }

      LayoutConfig modelLayoutParams = modelView.getLayoutConfig();
      if (useStubLayoutParams) {
        stubData.viewGroup
            .addComponent(modelView, stubData.position, stubData.viewStub.getLayoutConfig());
      } else if (modelLayoutParams != null) {
        stubData.viewGroup.addComponent(modelView, stubData.position, modelLayoutParams);
      } else {
        stubData.viewGroup.addComponent(modelView, stubData.position);
      }

      return modelView;
    }

    private ViewStubData getNextViewStubPosition(ComponentContainer viewGroup) {
      int childCount = viewGroup.getChildCount();
      for (int i = 0; i < childCount; i++) {
        Component child = viewGroup.getComponentAt(i);

        if (child instanceof ComponentContainer) {
          if(((ComponentContainer) child).getChildCount() == 0) {
            return new ViewStubData(viewGroup, (ComponentContainer) child, i);
          } else {
            ViewStubData nestedResult = getNextViewStubPosition((ComponentContainer) child);
            if (nestedResult != null) {
              return nestedResult;
            }
          }
        } else if (child instanceof ComponentContainer) {
          return new ViewStubData(viewGroup, (ComponentContainer) child, i);
        }
      }

      return null;
    }
  }

  private static class ViewStubData {

    private final ComponentContainer viewGroup;
    private final ComponentContainer viewStub;
    private final int position;

    private ViewStubData(ComponentContainer viewGroup, ComponentContainer viewStub, int position) {
      this.viewGroup = viewGroup;
      this.viewStub = viewStub;
      this.position = position;
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof EpoxyModelGroup)) {
      return false;
    }
    if (!super.equals(o)) {
      return false;
    }

    EpoxyModelGroup that = (EpoxyModelGroup) o;

    return models.equals(that.models);
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + models.hashCode();
    return result;
  }
}
